package com.sadhoc.router;

import java.io.IOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import com.sadhoc.router.OnMessageReceiverI;
import com.sadhoc.router.duplicate.DuplicateTable;
import com.sadhoc.router.duplicate.DuplicateTableEntry;
import com.sadhoc.router.neighbor.NeighborTable;
import com.sadhoc.router.pdu.HDPacket;
import com.sadhoc.router.pdu.HDPacketTypes;
import com.sadhoc.router.pdu.HelloMsg;
import com.sadhoc.router.pdu.TCMsg;
import com.sadhoc.router.routing.RouteEntry;
import com.sadhoc.router.routing.RoutingTable;
import com.sadhoc.router.topology.NodePair;
import com.sadhoc.router.topology.TopologyTable;
import com.sadhoc.router.util.Debug;
import com.sadhoc.router.util.DebugReceiverI;
import com.sadhoc.router.util.SubnetUtils;
import com.sadhoc.router.exceptions.InvalidDestinationIPException;
import com.sadhoc.router.exceptions.InvalidPacketTypeException;
import com.sadhoc.router.exceptions.NextHopNotFoundException;

public class SadhocRouter implements Runnable, OnMessageReceiverI {

	private PacketReceiver r 							= null;
	private PacketSender s 								= null;
	private OnMessageReceiverI recvr 					= null;
	private TopologyTable topologyTable 				= null;
	private NeighborTable neighborTable 				= null;
	private DuplicateTable duplicateTable 				= null;
	private RoutingTable routingTable 					= null;
	private Queue<HDPacket> queue 						= null;
	private ControllerThread cThread	 				= null;
	private ApplicationNotifierThread aNotifierThread	= null;
	private boolean running								= true;
	private boolean canRun								= true;
		
	/*
	 * Constructor starts the hocDroid router.
	 * Provide a class which implements OnMessageReceiverI,
	 * messages received not intended for the routing protocol will be called
	 */
	public SadhocRouter(String ip, String mask, int port, OnMessageReceiverI recvr, DebugReceiverI debgr) throws IOException {
		
		Constants.ip = ip;
		Constants.port = port;
		this.recvr = recvr;
		this.topologyTable = new TopologyTable();
		this.duplicateTable = new DuplicateTable();
		this.routingTable = new RoutingTable();
		try {
			this.r = new PacketReceiver(Constants.ip, Constants.port, this);
		} catch (IOException e) {
			canRun = false;
			throw e;
		}

		Constants.subnetInfo = new SubnetUtils(ip, mask).getInfo();
		Constants.network = Constants.subnetInfo.getNetworkAddress();
		Constants.broadcast = Constants.subnetInfo.getBroadcastAddress();
		
		queue = new ConcurrentLinkedQueue<HDPacket>();
		
		Debug.debgr = debgr;
		Debug.printBasic("HocDroid router starting");
	}

	/*
	 * Start point. Creates new Packet sender/receivers, starts the routing protocol
	 * by processing all received messages
	 */
	public void run() {

		if (!canRun) return;
		
		try {
			
			new Thread(r).start();
			s = new PacketSender(Constants.port);
									
			cThread = new ControllerThread();
			cThread.start();
			
			aNotifierThread = new ApplicationNotifierThread(recvr);
			aNotifierThread.start();
			
			neighborTable = new NeighborTable(this, topologyTable, s);
			
		} catch (SocketException e) {
			e.printStackTrace();
		}

		while (running) {
			try {
				
				synchronized(queue) {
					while (queue.isEmpty()) {
						queue.wait();
					}
				}

				HDPacket hdp = queue.poll();
				String destIP = hdp.getDestinationIP();
				boolean self = (hdp.getSourceIP().equals(Constants.ip));
				
				// Internal message from within the routing protocol
				if  (hdp.getTransport() == HDPacketTypes.INTERNAL) {
					
					switch (hdp.getType()) {
						case HDPacketTypes.HOCDROID_NEIGHBOR_LOST:
						s.closeSocket(hdp.getDestinationIP());
						break;
					}
					
					aNotifierThread.add(hdp);
					
				}
				// Destined to us or equal to the broadcast
				else if (((destIP.equals(Constants.ip)) || (destIP.equals(Constants.broadcast))) &&  (!self))
				{
					switch (hdp.getType()) {
					
						case HDPacketTypes.HOCDROUTER_ROUTER_HELLO:
							handler_Hello(hdp);
							break;
							
						case HDPacketTypes.HOCDROUTER_ROUTER_TC:
							
							handler_TC(hdp);
							break;
													
						default:
							aNotifierThread.add(hdp);
						
					}
				}
				// everything else
				else if (!self) {
					
					String nextHop = getNextHop(destIP);
					if (nextHop != null) {
						if (hdp.getTransport() == HDPacketTypes.TCP) {
							s.sendTCP(nextHop, hdp);
							Debug.printDetailed("Forwarding HDP from " + hdp.getSourceIP() + "to nexthop " + nextHop + " via TCP");
						}
						else if (hdp.getTransport() == HDPacketTypes.UDP) {
							s.sendUDP(nextHop, hdp);
							Debug.printDetailed("Forwarding HDP from " + hdp.getSourceIP() + "to nexthop " + nextHop + " via UDP");
						}
					}
					
				}		
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

	}
	
	/*
	 * This method stops everything 
	 */
	public void stop() {
		running = false;
		r.stop();
		s.stop();
		cThread.running = false;
		aNotifierThread.running = false;
	
	}
	
    /*
     * method for application to send TCP messages to a destination. The data will
     * be encapsulated into a HDPacket and sent to the next-hop destination
     */
    public void sendTCP(String destIP, byte type, byte[] data) throws UnknownHostException, InvalidPacketTypeException, InvalidDestinationIPException, NextHopNotFoundException {
    	
    	if ((type >= HDPacketTypes.RESERVED_MIN) && (type <= HDPacketTypes.RESERVED_MAX))
    		throw new InvalidPacketTypeException("HocDroid Packet type must be greater than " + HDPacketTypes.RESERVED_MAX);
            
		sendTCP(destIP, new HDPacket(destIP, Constants.ip, HDPacketTypes.TCP, type, data));
            
    }
    
    /*
     * method for application to send TCP messages to a destination. The data will
     * be encapsulated into a HDPacket and sent to the next-hop destination
     */
    public void sendTCP(String destIP, byte type, Object obj) throws UnknownHostException, InvalidPacketTypeException, InvalidDestinationIPException, NextHopNotFoundException {

    	if ((type >= HDPacketTypes.RESERVED_MIN) && (type <= HDPacketTypes.RESERVED_MAX))
    		throw new InvalidPacketTypeException("HocDroid Packet type must be greater than " + HDPacketTypes.RESERVED_MAX);
    	
		sendTCP(destIP, new HDPacket(destIP, Constants.ip, HDPacketTypes.TCP, type, obj));
            
    }

    /*
     * method for router to send tcp hdp packets
     */
    private void sendTCP(String destIP, HDPacket hdp) throws InvalidDestinationIPException, NextHopNotFoundException {
    	
        if (!Constants.subnetInfo.isInRange(destIP))
        	throw new InvalidDestinationIPException("Destination IP address is not within range of current subnet");
        
        if ((destIP.equals(Constants.network)) || (destIP.equals(Constants.broadcast)) || (destIP.equals("255.255.255.255")))
        	throw new InvalidDestinationIPException("Destination IP address cannot be a network or broadcast when using TCP");
    	
    	String nextHop = getNextHop(destIP);
        if (nextHop == null) 
        	throw new NextHopNotFoundException("Next hop IP for " + destIP + " not found");
        
	    s.sendTCP(nextHop, hdp);     
    	
    }
    
    /*
     * method for application to send UDP messages to a destination. The data will
     * be encapsulated into a HDPacket and sent to the next-hop destination
     */
    public void sendUDP(String destIP, byte type, byte[] data) throws UnknownHostException, InvalidPacketTypeException, NextHopNotFoundException {
            
    	if ((type >= HDPacketTypes.RESERVED_MIN) && (type <= HDPacketTypes.RESERVED_MAX))
    		throw new InvalidPacketTypeException("HocDroid Packet type must be greater than " + HDPacketTypes.RESERVED_MAX);
    	
		sendUDP(destIP, new HDPacket(destIP, Constants.ip, HDPacketTypes.UDP, type, data));
            
    }
    
    /*
     * method for application to send UDP messages to a destination. The data will
     * be encapsulated into a HDPacket and sent to the next-hop destination
     */
    public void sendUDP(String destIP, byte type, Object obj) throws UnknownHostException, InvalidPacketTypeException, NextHopNotFoundException {
            
    	if ((type >= HDPacketTypes.RESERVED_MIN) && (type <= HDPacketTypes.RESERVED_MAX))
    		throw new InvalidPacketTypeException("HocDroid Packet type must be greater than " + HDPacketTypes.RESERVED_MAX);
    	
    	sendUDP(destIP, new HDPacket(destIP, Constants.ip, HDPacketTypes.UDP, type, obj));
            
    }


    /*
     * method for router to send udp hdp packets
     */
    private void sendUDP(String destIP, HDPacket hdp) throws NextHopNotFoundException {
        
        if (!Constants.subnetInfo.isInRange(destIP))
        	return;
        
        String nextHop = getNextHop(destIP);
        if (nextHop == null) 
        	throw new NextHopNotFoundException("Next hop IP for " + destIP + " not found");
        
        s.sendUDP(nextHop, hdp);
        
    }

		
	/*
	 * Performs a routing table lookup to determine the next hop to a destination
	 */
	private String getNextHop(String destIP) {
		
		RouteEntry re = routingTable.getRoute(destIP);
		if (re == null)
			return null;
		
		return re.getNextHop();
		
	}
	
	/*
	 * Receives the HDMessages and passes to queue for processing
	 */
	public void onMessageRecv(HDPacket hdp) {
		
		queue.add(hdp);
		synchronized (queue) {
			queue.notify();
		}
			
	}

	/*
	 * Processes hello messages
	 */
	private void handler_Hello(HDPacket hdp) {
						
		Debug.printDetailed("Received hello message from " + hdp.getSourceIP());
		
		neighborTable.handleHelloMsg(hdp.getSourceIP(), (HelloMsg) hdp.getDataAsObject());

	}
	
	public HashSet<String> getAllNodes() {
		
		HashSet<String> nodes = new HashSet<String>();
		for(RouteEntry x : routingTable.getAllEntries())
			nodes.add(new String(x.getDestinationIP()));
		
		return nodes; 
		
	}
	
	/*
	 * Processes TC messages
	 */
	private void handler_TC(HDPacket hdp) {

		Debug.printDetailed("Received tc message from " + hdp.getSourceIP());
		
		TCMsg tcMsg = (TCMsg) hdp.getDataAsObject();
				
		if (!neighborTable.isNeighborSym(hdp.getSourceIP())) {
			Debug.printDetailed("Discarding tc message - neighbor is not SYM");
			return;
		}	
		else if (tcMsg.getOriginator().equals(Constants.ip)) {
			Debug.printDetailed("Discarding tc message - I sourced the message");
			return;
		}
		
		topologyTable.handleTCMsg(hdp.getSourceIP(), tcMsg);
		duplicateTable.addEntry(new DuplicateTableEntry(tcMsg.getOriginator(), tcMsg.getSequenceNumber()), false, Constants.DUPLICATE_TIMEOUT);
		
		if (duplicateTable.contains(tcMsg.getOriginator(), tcMsg.getSequenceNumber())) {
			Debug.printDetailed("Discarding tc message - found in duplicate table");
			return;
		}
		
		Debug.printBasic("Re-transmitting tc msg");
		tcMsg.increaseHopCount();
		try {
			s.sendUDP(Constants.broadcast, new HDPacket(Constants.broadcast, Constants.ip, HDPacketTypes.UDP, HDPacketTypes.HOCDROUTER_ROUTER_TC, tcMsg));
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}

	}
	
	/*
	 * Returns a copy of the topology table
	 */
	public TopologyTable getCopyOfTopologyTable() {
		
		return topologyTable.clone();
		
	}
	
	/*
	 * Returns a copy of the routing table
	 */	
	public RoutingTable getCopyOfRoutingTable() {
		
		return routingTable.clone();
		
	}	

	/*
	 * Returns a copy of the neighboer table
	 */
	public NeighborTable getCopyOfNeighborTable() {
		
		return neighborTable.clone();
		
	}	
	
	/*
	 * Returns a copy of the duplicate table
	 */
	public DuplicateTable getCopyOfDuplicateTable() {
		
		return duplicateTable.clone();
		
	}	
	
	/*
	 * Router thread for transmitting hello and TC messages, as well any re-calculations required.
	 */
	public class ControllerThread extends Thread {
		public boolean running = true;				
		public void run() {
			
			// To-do: should implement a better time tracking mechanism - A.Stone
			int helloCount = 0;
			int tcCount = 0;
			
			HashSet<String> symNeighbors = null;
			TopologyTable tTable = null;
			RoutingTable rTable = new RoutingTable();

			int i = 0;
			
			while (running) {
				
				try {
					Thread.sleep(Constants.CONTROLLER_SLEEP);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				i++;
				helloCount += Constants.CONTROLLER_SLEEP;
				tcCount += Constants.CONTROLLER_SLEEP;
				
				if (i == 10) {
					i = 0;
					neighborTable.decreaseAndRemoveEntry(Constants.CONTROLLER_SLEEP * 10);
					topologyTable.decreaseAndRemoveEntry(Constants.CONTROLLER_SLEEP * 10);
					duplicateTable.decreaseAndRemoveEntry(Constants.CONTROLLER_SLEEP * 10);
				}
												
				if (neighborTable.isChangedFlagSet() || topologyTable.isChangedFlagSet()) {
					if (neighborTable.isChangedFlagSet())
						Debug.printBasic("Neighbor table has changed. Recalculating routing table");
					else
						Debug.printBasic("Topology table has changed. Recalculating routing table");
					
					neighborTable.setChangedFlag(false);
					topologyTable.setChangedFlag(false);
					
					symNeighbors = neighborTable.cloneSymNeighbors();
					tTable = topologyTable.clone();
					
					// Added all of our neighbors first.
					for (String neighbor : symNeighbors) {
						rTable.addRoute(new RouteEntry(neighbor, neighbor, 1));
					}
					
					// Add routes which have a hop equal to curHop until we stopped adding routes.
					int curHop = 1;
					boolean repeat;
					do {
						repeat = false;
						for (NodePair pair : tTable.keySet()) {
						
							String originator = pair.getOriginator();
							String advertiser = pair.getAdvertisedNode();
							
							if (advertiser.equals(Constants.ip))
								continue;
						
							RouteEntry entryOriginator = rTable.getRoute(originator);
							RouteEntry entryAdvertiser = rTable.getRoute(advertiser);
						
							if ((entryOriginator != null) && (entryAdvertiser == null)) {
								if (entryOriginator.getMetric() == curHop) {
									rTable.addRoute(new RouteEntry(advertiser, originator, curHop+1));
									repeat = true;
								}
							}
							
						}
					} while (repeat);
					
					routingTable.setCopyOfRoutingTable(rTable);
					rTable.clear();
					
					Debug.printBasic("\n\t\t Routing Table\n");
					Debug.printBasic(routingTable.toString());
					
					Debug.printBasic("\n\t\t Topology Table\n");
					Debug.printBasic(topologyTable.toString());
					
					try {
						recvr.onMessageRecv(new HDPacket(null, "0.0.0.0", HDPacketTypes.INTERNAL, HDPacketTypes.HOCDROID_TOPOLOGY_CHANGE, null));
					} catch (UnknownHostException e) {
						e.printStackTrace();
					}
					
				}
				
				if (helloCount >= Constants.HELLO_INTERVAL) {
					try {
						Debug.printDetailed("Sending hello message");
						s.sendUDP(Constants.broadcast, new HDPacket(Constants.broadcast, Constants.ip, HDPacketTypes.UDP, HDPacketTypes.HOCDROUTER_ROUTER_HELLO, neighborTable.generateHelloMsg()));
						helloCount = 0;
					} catch (UnknownHostException e) {
						e.printStackTrace();
					}
				}
								
				if (tcCount >= Constants.TC_INTERVAL) {
					try {
						Debug.printDetailed("Sending tc message");
						s.sendUDP(Constants.broadcast, new HDPacket(Constants.broadcast, Constants.ip, HDPacketTypes.UDP, HDPacketTypes.HOCDROUTER_ROUTER_TC, neighborTable.generateTCMsg()));
						tcCount = 0;
					} catch (UnknownHostException e) {
						e.printStackTrace();
					}
				}
	
			}
		}
		
	}
	
}
